/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkField, Stmt, ArkAssignStmt, ArkUnopExpr, IRUtils } from 'arkanalyzer/lib';
import { ClassCategory } from 'arkanalyzer/lib/core/model/ArkClass';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseMetaData, BaseChecker } from '../BaseChecker';
import { Defects, IssueReport } from '../../model/Defects';
import {
    ClassMatcher,
    MatcherCallback,
    MatcherTypes,
    FieldMatcher, FileMatcher,
} from '../../matcher/Matchers';
import { RuleListUtil } from '../../utils/common/DefectsList';
import { Rule } from '../../model/Rule';
import { ArkClass, ArkFile, Value } from 'arkanalyzer';
import { CompositeMatcher } from '../../matcher/matcherAdapter/matchCommon';
import { NumberConstant, StringConstant } from 'arkanalyzer/lib/core/base/Constant';
import { AbstractBinopExpr } from 'arkanalyzer/lib/core/base/Expr';
import { Language } from 'arkanalyzer/lib/core/model/ArkFile';
import { ArkMetadataKind, EnumInitTypeUserMetadata } from 'arkanalyzer/lib/core/model/ArkMetadata';

const logger = Logger.getLogger(
    LOG_MODULE_TYPE.HOMECHECK,
    'PreferLiteralEnumMemberCheck'
);
const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: 'docs/prefer-literal-enum-member.md',
    description:
        'Explicit enum value must only be a literal value (string, number, boolean, etc).',
};

interface Options {
    allowBitwiseExpressions?: boolean;
}

const defaultOptions: Options = {
    allowBitwiseExpressions: false,
};
const bitWiseOperators = new Set<string>(['<<', '>>', '>>>', '&', '|', '^', '~']);

//推荐显式初始化每个枚举成员值。
export class PreferLiteralEnumMemberCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
        match(arkFile: ArkFile): boolean {
            const lang = arkFile.getLanguage();
            return lang === Language.JAVASCRIPT || lang === Language.TYPESCRIPT;
        }
    };

    private classMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS,
        match(arkClass: ArkClass): boolean {
            return arkClass.getCategory() === ClassCategory.ENUM;
        }
    };

    private fieldMatcher: FieldMatcher = {
        matcherType: MatcherTypes.FIELD,
    };

    private mixMatcher: CompositeMatcher = new CompositeMatcher([this.fileMatcher, this.classMatcher, this.fieldMatcher]);

    public registerMatchers(): MatcherCallback[] {
        const matchBuildTs: MatcherCallback = {
            matcher: this.mixMatcher,
            callback: this.check,
        };
        return [matchBuildTs];
    }

    //要求所有枚举成员都定义为字面量值。
    public check = (targetField: ArkField) => {
        const severity = this.rule.alert ?? this.metaData.severity;
        const options: Options = this.rule.option[0] as unknown as Options;
        const mergedOptions = {
            ...defaultOptions,
            ...options,
        };
        const allowBitwiseExpressions = mergedOptions.allowBitwiseExpressions ?? false;
        const stmts = targetField.getInitializer() ?? [];
        // 无初始值
        let noInit: boolean;
        if (stmts.length === 0) {
            noInit = true;
        } else {
            const metadata = targetField.getMetadata(ArkMetadataKind.ENUM_INIT_TYPE_USER);
            noInit = metadata instanceof EnumInitTypeUserMetadata && !metadata.isUserDefined();
        }
        if (noInit) {
            return;
        }
        // 直接赋值， 没有任何运算，只检查是否数字常量或者字符串常量
        if (stmts.length === 1 && this.checkNumberOrStringConstant(stmts[0])) {
            return;
        }
        // 长度大于1，存在运算符、其它类型表达式等情况，允许位操作则检查运算符和操作值，不允许位操作则直接告警
        if (stmts.length > 1 && allowBitwiseExpressions && this.checkInitStmts(stmts)) {
            return;
        }
        this.addIssueReport(targetField, severity);
    };

    private checkNumberOrStringConstant(stmt: Stmt): boolean {
        if (!(stmt instanceof ArkAssignStmt)) {
            return false;
        }
        const rightOp = stmt.getRightOp();
        return rightOp instanceof NumberConstant || rightOp instanceof StringConstant;
    }

    private checkOperator(expr: ArkUnopExpr | AbstractBinopExpr): boolean {
        return bitWiseOperators.has(expr.getOperator());
    }

    private checkOp(op: Value): boolean {
        return IRUtils.isTempLocal(op) || op instanceof NumberConstant;
    }

    private checkInitStmt(stmt: Stmt): boolean {
        if (!(stmt instanceof ArkAssignStmt)) {
            return false;
        }
        const rightOp = stmt.getRightOp();
        if (IRUtils.isTempLocal(rightOp)) {
            return true;
        }
        if (rightOp instanceof ArkUnopExpr) {
            if (!this.checkOperator(rightOp)) {
                return false;
            }
            return this.checkOp(rightOp.getOp());
        }
        if (rightOp instanceof AbstractBinopExpr) {
            if (!this.checkOperator(rightOp)) {
                return false;
            }
            return this.checkOp(rightOp.getOp1()) && this.checkOp(rightOp.getOp2());
        }
        return false;
    }

    private checkInitStmts(stmts: Stmt[]): boolean {
        for (const stmt of stmts) {
            if (!this.checkInitStmt(stmt)) {
                return false;
            }
        }
        return true;
    }

    private addIssueReport(arkField: ArkField, severity: number): void {
        const position = arkField.getOriginPosition();
        const fieldNameLength = arkField.getName().length;
        let defect = new Defects(
            position.getLineNo(),
            position.getColNo(),
            position.getColNo() + fieldNameLength,
            this.metaData.description,
            severity,
            this.rule.ruleId,
            arkField.getDeclaringArkClass().getDeclaringArkFile().getFilePath(),
            this.metaData.ruleDocPath,
            true,
            false,
            false
        );
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}
